/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Message types are registered with the {@code MessageRegistry} class. Once a message type has been registered, it can
 * be used on the network.
 *
 * TODO: write a more detailed description
 */
public class MessageRegistry {
	/**
	 * The {@code MessageFactory} interface serves as a factory and should be implemented for each subclass of
	 * {@link Message}. The method {@link MessageRegistry.MessageFactory#create()} should be overridden to return a new
	 * instance of the particular {@link Message} subclass. The method {@link MessageRegistry.MessageFactory#destroy
	 * (Message)} is called when a message instance is no longer needed and can be overridden to implement pooling.
	 */
	public interface MessageFactory {
		/**
		 * Should be overridden to return a new instance of a {@link Message} subclass.
		 *
		 * @return  a new instance of a {@link Message} subclass.
		 */
		public Message create();

		/**
		 * Called when an existing message is no longer in use. This method allows for optional pooling.
		 *
		 * @param   message the message which is no longer in use.
		 */
		public void destroy(Message message);
	}

	/**
	 * The {@code StateTableFactory} interface serves as a factory and should be implemented for each subclass of
	 * {@link StateTable}. The method {@link MessageRegistry.StateTableFactory#create()} should be overridden to
	 * return a new instance of the particular {@link StateTable} subclass. The method {@link MessageRegistry
	 * .StateTableFactory#destroy(StateTable)} is called when a state table instance is no longer needed and can be
	 * overridden to implement pooling.
	 */
	public interface StateTableFactory {
		/**
		 * Should be overridden to return a new instance of a {@link StateTable} subclass.
		 *
		 * @return  a new instance of a {@link StateTable} subclass.
		 */
		public StateTable create();

		/**
		 * Called when an existing state table is no longer in use. This method allows for optional pooling.
		 *
		 * @param   stateTable the state table which is no longer in use.
		 */
		public void destroy(StateTable stateTable);
	}

	/**
	 * Initializes fields.
	 */
	public MessageRegistry() {
		messageTypeData = new HashMap<Integer, MessageTypeData>();
		stateTableFactories = new HashMap<Integer, StateTableFactory>();
	}

	/**
	 * Registers a new type of message for use on the network.
	 *
	 * @param   id the ID of the message type. The ID should not be negative.
	 * @param   messageFactory an implementation of {@link MessageFactory} used to create instances of the message type.
	 * @throws  IllegalArgumentException if a message or state table type with the provided ID has already been
	 *          registered, or if the ID is negative.
	 * @throws  IllegalStateException if registration has already been finalized.
	 */
	public void register(int id, MessageFactory messageFactory) {
		if (networkIdMap != null) {
			throw new IllegalStateException("Registration has already been finalized");
		}
		if (id < 0) {
			throw new IllegalArgumentException("Negative ID " + id + " provided");
		}
		if (messageTypeData.containsKey(id)) {
			throw new IllegalArgumentException("Message type " + id + " already registered");
		}

		MessageTypeData mtd = new MessageTypeData();
		mtd.messageFactory = messageFactory;
		mtd.registeredType = RegisteredType.MESSAGE;
		messageTypeData.put(id, mtd);
	}

	/**
	 * Registers a new type of state table for use the network.
	 *
	 * @param   id the ID of the state table type. The ID should not be negative.
	 * @param   stateTableFactory an implementation of {@link StateTableFactory} used to create instances of the state
	 *          table type.
	 * @throws  IllegalArgumentException if a message or state table type with the provided ID has already been
	 *          registered, or if the ID is negative.
	 * @throws  IllegalStateException if registration has already been finalized.
	 */
	public void register(int id, StateTableFactory stateTableFactory) {
		if (networkIdMap != null) {
			throw new IllegalStateException("Registration has already been finalized");
		}
		if (id < 0) {
			throw new IllegalArgumentException("Negative ID " + id + " provided");
		}
		if (messageTypeData.containsKey(id)) {
			throw new IllegalArgumentException("State table type " + id + " already registered");
		}

		// use StateTableMessageFactory to create the specific message type for state tables
		MessageTypeData mtd = new MessageTypeData();
		mtd.messageFactory = new StateTableMessageFactory(id);
		mtd.registeredType = RegisteredType.STATE_TABLE;
		messageTypeData.put(id, mtd);

		// register state table factory as well
		stateTableFactories.put(id, stateTableFactory);
	}

	/**
	 * Condenses message and state table type IDs to use the smallest possible range. This method should be called once
	 * after all message and state table types have been registered.
	 *
	 * @throws  IllegalStateException if registration has already been finalized.
	 */
	public void finalizeRegistration() {
		if (networkIdMap != null) {
			throw new IllegalStateException("Registration has already been finalized");
		}

		networkIdMap = new HashMap<Integer, Integer>();

		// sort IDs
		int[] sortedIds = new int[messageTypeData.size()];
		Iterator<Integer> userIdIt = messageTypeData.keySet().iterator();
		int next = 0;
		while (userIdIt.hasNext()) {
			sortedIds[next] = userIdIt.next();
			++next;
		}
		Arrays.sort(sortedIds);

		// create network ID mapping for each
		for (int i = 0; i < sortedIds.length; ++i) {
			// update message's network ID
			messageTypeData.get(sortedIds[i]).networkId = i;
			// map network ID to user ID
			networkIdMap.put(i, sortedIds[i]);
		}

		// determine number of bits required
		networkIdBits = 1;
		int idRange = 2;
		// keep incrementing bit count until we cover the whole range of IDs
		while (idRange < networkIdMap.size()) {
			++networkIdBits;
			idRange *= 2;
		}
	}

	/**
	 * Returns whether the message registry has been finalized.
	 *
	 * @return  whether the message registry has been finalized.
	 */
	public boolean isFinalized() {
		return networkIdMap != null;
	}

	/**
	 * Returns the assigned network message ID corresponding to the user-provided message ID.
	 *
	 * @param   messageId the user-provided message ID.
	 * @return  the corresponding network ID, or -1 if the given message ID was never registered.
	 */
	public int getNetworkMessageId(int messageId) {
		Integer nwId = networkIdMap.get(messageId);
		return (nwId == null) ? -1 : nwId;
	}

	/**
	 * Returns the user-provided message ID corresponding to the given network ID.
	 *
	 * @param   networkMessageId the network ID.
	 * @return  the corresponding user-provided ID, or -1 if the given network ID is invalid.
	 */
	public int getMessageId(int networkMessageId) {
		MessageTypeData mtd = messageTypeData.get(networkMessageId);
		return (mtd == null) ? -1 : mtd.networkId;
	}

	/**
	 * Returns the number of bits required for network IDs. This value depends on the number of message types
	 * registered.
	 *
	 * @return  the number of bits required for network IDs.
	 */
	public int getNetworkIdBits() {
		return networkIdBits;
	}

	/**
	 * Returns the type that the given ID was registered as.
	 *
	 * @param   id the ID for which the registered type should be returned.
	 * @return  the type the ID was registered as.
	 * @throws  IllegalArgumentException if ID has not been registered.
	 */
	public RegisteredType getRegisteredType(int id) {
		MessageTypeData mtd = messageTypeData.get(id);
		if (mtd == null) {
			throw new IllegalArgumentException("Invalid message type");
		}
		return mtd.registeredType;
	}

	/**
	 * Describes what type an ID was registered as.
	 */
	public enum RegisteredType {
		/**
		 * An ID which was registered as a message.
		 */
		MESSAGE,

		/**
		 * An ID which was registered as a state table.
		 */
		STATE_TABLE
	}

	/**
	 * Calls {@link MessageRegistry.MessageFactory#create()} for the message type provided by {@code messageId}.
	 *
	 * @param   messageId the ID of the message to create.
	 * @return  an instance of the message type registered with {@code messageId}.
	 * @throws  IllegalArgumentException if {@code messageId} hasn't been registered.
	 * @throws  IllegalStateException if the ID returned from calling {@link Message#getId()} on the newly created
	 *          message does not match the ID provided.
	 */
	Message createMessage(int messageId) {
		MessageTypeData mtd = messageTypeData.get(messageId);
		if (mtd == null) {
			throw new IllegalArgumentException("Invalid message type");
		}
		Message msg = mtd.messageFactory.create();
		if (msg.getId() != messageId) {
			throw new IllegalStateException("Message ID from instance does not match message ID provided");
		}
		return msg;
	}

	/**
	 * Calls {@link MessageRegistry.MessageFactory#destroy(Message)} for the message type and instance provided by
	 * {@code message}.
	 *
	 * @param   message the message to destroy/free.
	 * @throws  IllegalArgumentException if the ID returned by calling {@link Message#getId()} on {@code message}
	 *          hasn't been registered.
	 */
	void destroyMessage(Message message) {
		MessageTypeData mtd = messageTypeData.get(message.getId());
		if (mtd == null) {
			throw new IllegalArgumentException("Invalid message type");
		}
		mtd.messageFactory.destroy(message);
	}

	/**
	 * Calls {@link MessageRegistry.StateTableFactory#create()} for the state table type provided by {@code
	 * stateTableId}.
	 *
	 * @param   stateTableId the ID of the state table to create.
	 * @return  an instance of the state table type registered with {@code stateTableId}.
	 * @throws  IllegalArgumentException if {@code stateTableId} hasn't been registered or wasn't registered as a state
	 *          table.
	 * @throws  IllegalStateException if the ID returned from calling {@link StateTable#getId()} on the newly created
	 *          state table does not match the ID provided.
	 */
	StateTable createStateTable(int stateTableId) {
		StateTableFactory stf = stateTableFactories.get(stateTableId);
		if (stf == null) {
			throw new IllegalArgumentException("Invalid state table type");
		}
		StateTable st = stf.create();
		if (st.getId() != stateTableId) {
			throw new IllegalStateException("State table ID from instance does not match state table ID provided");
		}
		return st;
	}

	/**
	 * Calls {@link MessageRegistry.StateTableFactory#destroy(StateTable)} for the state table type and instance
	 * provided by {@code stateTable}.
	 *
	 * @param   stateTable the state table to destroy/free.
	 * @throws  IllegalArgumentException if the ID returned by calling {@link StateTable#getId()} on {@code message}
	 *          hasn't been registered or wasn't registered as a state table.
	 */
	void destroyStateTable(StateTable stateTable) {
		StateTableFactory stf = stateTableFactories.get(stateTable.getId());
		if (stf == null) {
			throw new IllegalArgumentException("Invalid state table type");
		}
		stf.destroy(stateTable);
	}

	// factory for creating state table messages
	private static class StateTableMessageFactory implements MessageFactory {
		public StateTableMessageFactory(int messageId) {
			this.messageId = messageId;
		}

		public Message create() {
			return new StateTableMessage(messageId);
		}

		// todo: could implement pooling if needed
		public void destroy(Message message) {}

		// ID of the messages/state tables
		private int messageId;
	}

	// stores data about a particular type of message
	private static class MessageTypeData {
		// used to instantiate and free the message instance
		public MessageFactory messageFactory;

		// registered type
		RegisteredType registeredType;

		// ID used to identify this message on the network
		// this is different from the ID provided by the user
		// network IDs are compressed into the smallest range possible to reduce the number of bits used
		public int networkId;
	}

	// maps user provided message IDs to message type data
	private HashMap<Integer, MessageTypeData> messageTypeData;

	// maps user provided state table IDs to state table factories
	private HashMap<Integer, StateTableFactory> stateTableFactories;

	// maps network message IDs to user message IDs
	private HashMap<Integer, Integer> networkIdMap;

	// number of bits required for network IDs
	private int networkIdBits;
}
